﻿using System;
using System.Web;
using RestSharp;
using Newtonsoft.Json;
using SilenthinkContract;
using Newtonsoft.Json.Linq;
using SilenthinkContract.Domain;
using System.Collections.Specialized;

namespace Web.OAuth.Client
{
    public abstract class OAuthClient : IOAuthClient
    {
        protected const string AccessTokenKey = "access_token";

        private readonly IRestClient client;
        private readonly OAuthConfig configuration;

        /// <summary>
        /// Friendly name of provider (OAuth2 service).
        /// </summary>
        public string ProviderName { get { return configuration.ClientName; } }
        
        /// <summary>
        /// Access token returned by provider. Can be used for further calls of provider API.
        /// </summary>
        public string AccessToken { get; private set; }

        /// <summary>
        /// State (any additional information that was provided by application and is posted back by service).
        /// </summary>
        public string State { get; private set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="OAuthClient"/> class.
        /// </summary>
        /// <param name="configuration">The configuration.</param>
        protected OAuthClient(OAuthConfig configuration)
        {
            this.configuration = configuration;
            client = new RestClient(configuration.BaseUri);
        }

        /// <summary>
        /// Returns URI of service which should be called in order to start authentication process.
        /// This URI should be used for rendering login link.
        /// </summary>
        /// <remarks>
        /// Any additional information that will be posted back by service.
        /// </remarks>
        public string GetLoginUri(string state = null)
        {            
            var request = new RestRequest(configuration.AuthorizationCodeResource);

            request.AddObject(new
            {
                response_type = "code",
                client_id = configuration.ClientId,
                redirect_uri = configuration.RedirectUri,
                scope = configuration.Scope,
                state
            });

            return client.BuildUri(request).ToString();
        }

        /// <summary>
        /// Obtains user information using OAuth2 service and
        /// data provided via callback request.
        /// </summary>
        /// <param name="parameters">Callback request payload (parameters).</param>
        public ExternalUser GetUserInfo(NameValueCollection parameters)
        {
            State = parameters["state"];

            var error = parameters["error"];
            if (!string.IsNullOrEmpty(error))
            {
                throw new ArgumentException(error);
            }
            AccessToken = GetAccessToken(parameters);
            return GetUserInfo(AccessToken);
        }

        /// <summary>
        /// Issues query for access token and parses response.
        /// </summary>
        /// <param name="parameters">Callback request payload (parameters).</param>
        private string GetAccessToken(NameValueCollection parameters)
        {
            var request = new RestRequest(configuration.AccessTokenResource, Method.POST);
            request.AddObject(new
            {
                code = parameters["code"],
                client_id = configuration.ClientId,
                client_secret = configuration.ClientSecret,
                redirect_uri = configuration.RedirectUri,
                grant_type = "authorization_code"
            });

            var response = client.Execute(request);
            AfterGetAccessToken(response);

            var content = response.Content;
            var token = string.Empty;
            try
            {
                // response can be sent in JSON format
                token= (string) JObject.Parse(content).SelectToken(AccessTokenKey);
            }
            catch (JsonReaderException e)
            {
                // or it can be in "query string" format (param1=val1&param2=val2)
                token= HttpUtility.ParseQueryString(content)[AccessTokenKey];
            }
            return token;
        }

        /// <summary>
        /// Obtains user information using provider API.
        /// </summary>
        /// <param name="accessToken">The access token.</param>
        private ExternalUser GetUserInfo(string accessToken)
        {
            //var authenticator = new OAuth2UriQueryParameterAuthenticator(accessToken);
            //client.Authenticator = authenticator;
          
            var request = new RestRequest(configuration.UserInfoResource);
            request.AddParameter(AccessTokenKey, accessToken);
            BeforeGetUserInfo(request);

            var result = ParseUserInfo(client.Execute(request).Content);
            result.ProviderName = ProviderName;

            return result;
        }

        /// <summary>
        /// Should return parsed <see cref="ExternalUser"/> using content received from provider.
        /// </summary>
        /// <param name="content">The content which is received from provider.</param>
        protected abstract ExternalUser ParseUserInfo(string content);

        /// <summary>
        /// Called just before issuing request to service when everything is ready.
        /// Allows to add extra parameters to request or do any other needed preparations.
        /// </summary>
        protected virtual void BeforeGetUserInfo(IRestRequest request)
        {
        }

        /// <summary>
        /// Called just after obtaining response with access token from service.
        /// Allows to read extra data returned along with access token.
        /// </summary>
        protected virtual void AfterGetAccessToken(IRestResponse response)
        {
        }
    }
}